﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using JasLib.Diagnostics;
using JasLib.Extensions;
using JasLib.Threading;

namespace JasLib.Interop.Win32
{
    /// <summary>
    /// This environment reporting class is a little more thorough than System.Environment.
    /// </summary>
    public static class Environment
    {
        private static readonly bool s_bIsWow64Process = false;
        private static readonly SYSTEM_INFO s_systemInfo = new SYSTEM_INFO();
        private static readonly OSVERSIONINFOEX s_OSVersionInfoEx = new OSVERSIONINFOEX();
        private static readonly UniversalVersion s_OSVersion = null;
        private static CountdownEvent s_COMThreadSensitiveInitializationCompleteEvent = null;

        private static string s_strFullOSName = null;

        static Environment()
        {
            if (!KERNEL32.IsWow64Process(KERNEL32.GetCurrentProcess(), out s_bIsWow64Process))
                throw new Win32Exception();

            if (s_bIsWow64Process)
                KERNEL32.GetNativeSystemInfo(out s_systemInfo);
            else
                KERNEL32.GetSystemInfo(out s_systemInfo);

            s_OSVersionInfoEx = new OSVERSIONINFOEX { dwOSVersionInfoSize = MarshalHelper.SizeOf<OSVERSIONINFOEX>() };
            if (!KERNEL32.GetVersionEx(ref s_OSVersionInfoEx))
                throw new Win32Exception();

            s_OSVersion = new UniversalVersion(0,
                (int)s_OSVersionInfoEx.dwMajorVersion,
                (int)s_OSVersionInfoEx.dwMinorVersion,
                (int)s_OSVersionInfoEx.dwPlatformId,
                (int)s_OSVersionInfoEx.dwBuildNumber);

            s_COMThreadSensitiveInitializationCompleteEvent = new CountdownEvent(2);
            InitializeMtaRequiredItemsAsync();
            InitializeStaRequiredItemsAsync();
            return;
        }

        #region Initialization specific to COM threading models

        /// <summary>
        /// These items must be initialized on an MTA thread.
        /// This runs on the thread pool, if done asynchronously.
        /// </summary>
        private static async void InitializeMtaRequiredItemsAsync()
        {
            try
            {
                if (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA)
                    await AsyncThreadSwitcher.UnsafeSwitchToThreadPoolAsync();

                ManagementObject managementObject = new ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem")
                    .Get()
                    .OfType<ManagementObject>()
                    .FirstOrDefault();
                if (managementObject != null)
                {
                    s_strFullOSName = "{0} {1} {2} {3}".FormatWith(
                        managementObject.GetPropertyValue("Caption"),
                        managementObject.GetPropertyValue("OSArchitecture"),
                        managementObject.GetPropertyValue("Version"),
                        managementObject.GetPropertyValue("CSDVersion"));

                    /// Remove unnecessary spaces. This is an unorthodox approach.
                    s_strFullOSName = s_strFullOSName.Split(StringSplitOptions.RemoveEmptyEntries, " ").Join(" ");
                }
            }
            catch
            {
            }
            finally
            {
                s_strFullOSName = s_strFullOSName.TrimToEmpty();

                SignalAsyncConstruction();
            }
            return;
        }

        /// <summary>
        /// These items must be initialized on an STA thread.
        /// This runs on a dedicated thread, if done asynchronously.
        /// </summary>
        private static async void InitializeStaRequiredItemsAsync()
        {
            try
            {
                if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
                {
                    await AsyncThreadSwitcher.SwitchToNewManagedThreadAsync(ApartmentState.STA);
                    Thread.CurrentThread.Name = "Interop.Environment STA type initialization thread";
                }
            }
            catch
            {
            }
            finally
            {
                SignalAsyncConstruction();
            }
            return;
        }

        private static void SignalAsyncConstruction()
        {
            s_COMThreadSensitiveInitializationCompleteEvent.Signal();
            if (s_COMThreadSensitiveInitializationCompleteEvent.CurrentCount == 0)
                s_COMThreadSensitiveInitializationCompleteEvent = null;
            return;
        }

        private static void WaitForAsyncConstruction()
        {
            var countdownEvent = s_COMThreadSensitiveInitializationCompleteEvent;
            if (countdownEvent != null)
                countdownEvent.Wait();
            return;
        }

        #endregion

        public static string CommandLine
        {
            get { return Marshal.PtrToStringUni(KERNEL32.GetCommandLine()); }
        }

        public static class ComputerName
        {
            public static string DnsDomain
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNameDnsDomain); }
            }

            public static string DnsFullyQualified
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNameDnsFullyQualified); }
            }

            public static string DnsHostname
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNameDnsHostname); }
            }

            public static string NetBIOS
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNameNetBIOS); }
            }

            public static string PhysicalDnsDomain
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalDnsDomain); }
                set
                {
                    if (!KERNEL32.SetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalDnsDomain, value))
                        throw new Win32Exception();
                }
            }

            public static string PhysicalDnsFullyQualified
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalDnsFullyQualified); }
            }

            public static string PhysicalDnsHostname
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalDnsHostname); }
                set
                {
                    if (!KERNEL32.SetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalDnsHostname, value))
                        throw new Win32Exception();
                }
            }

            public static string PhysicalNetBIOS
            {
                get { return KERNEL32.GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalNetBIOS); }
                set
                {
                    if (!KERNEL32.SetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNamePhysicalNetBIOS, value))
                        throw new Win32Exception();
                }
            }
        }

        public static bool IsWindows2000OrGreater
        {
            get { return (UniversalVersion.Compare(s_OSVersion, 5, 0) >= 0); }
        }

        public static bool IsWindowsXPOrGreater
        {
            get { return (UniversalVersion.Compare(s_OSVersion, 5, 1) >= 0); }
        }

        public static bool IsWindowsVistaOrGreater
        {
            get { return (UniversalVersion.Compare(s_OSVersion, 6, 0) >= 0); }
        }

        public static bool IsWindows7OrGreater
        {
            get { return (UniversalVersion.Compare(s_OSVersion, 6, 1) >= 0); }
        }

        public static bool IsWow64Process
        {
            get { return s_bIsWow64Process; }
        }

        public static int NumberOfProcessors
        {
            get { return (int)s_systemInfo.dwNumberOfProcessors; }
        }

        public static string FullOSName
        {
            get
            {
                WaitForAsyncConstruction();
                return s_strFullOSName;
            }
        }

        public static int PageSize
        {
            get { return (int)s_systemInfo.dwPageSize; }
        }

        public static long PerformanceCounter
        {
            get { return KERNEL32.QueryPerformanceCounter(); }
        }

        public static long PerformanceFrequency
        {
            get { return KERNEL32.QueryPerformanceFrequency(); }
        }

        public static int ServicePackMajor
        {
            get { return s_OSVersionInfoEx.wServicePackMajor; }
        }

        public static int ServicePackMinor
        {
            get { return s_OSVersionInfoEx.wServicePackMinor; }
        }

        public static uint SystemRegistryQuotaAllowed
        {
            get
            {
                uint uiQuotaAllowed;
                uint uiQuotaUsed;
                if (!KERNEL32.GetSystemRegistryQuota(out uiQuotaAllowed, out uiQuotaUsed))
                    throw new Win32Exception();
                return uiQuotaAllowed;
            }
        }

        public static uint SystemRegistryQuotaUsed
        {
            get
            {
                uint uiQuotaAllowed;
                uint uiQuotaUsed;
                if (!KERNEL32.GetSystemRegistryQuota(out uiQuotaAllowed, out uiQuotaUsed))
                    throw new Win32Exception();
                return uiQuotaUsed;
            }
        }

        public static string TempDirectory
        {
            get { return KERNEL32.GetTempPath(); }
        }

        /// <summary>
        /// The duration of time since Windows was last started.
        /// </summary>
        public static TimeSpan Uptime
        {
            get
            {
                try
                {
                    Win32.FILETIME idleTime, kernelTime, userTime;
                    if (!KERNEL32.GetSystemTimes(out idleTime, out kernelTime, out userTime))
                        throw new Win32Exception();

                    /// Kernel time includes idle time.
                    return TimeSpan.FromTicks(((long)kernelTime + (long)userTime) / s_systemInfo.dwNumberOfProcessors);
                }
                catch
                {
                    using (NtPerformanceCounterQueryAgent agent = new NtPerformanceCounterQueryAgent())
                    {
                        NtPerformanceCounter uptimeCounter = new NtPerformanceCounter(@"\\.\System\System Up Time");
                        agent.Counters.Add(uptimeCounter);
                        agent.Update();
                        return TimeSpan.FromSeconds(uptimeCounter.DoubleValue);
                    }
                }
            }
        }

        /// <summary>
        /// A read/writeable native alternative to <seealso cref="System.DateTime.UtcNow"/>.
        /// </summary>
        public static DateTime UtcNow
        {
            get
            {
                SYSTEMTIME systemTime;
                Win32.FILETIME fileTime;
                if (!KERNEL32.GetSystemTime(out systemTime) || !KERNEL32.SystemTimeToFileTime(ref systemTime, out fileTime))
                    throw new Win32Exception();

                return DateTime.FromFileTimeUtc(fileTime);
            }
            set
            {
                Win32.FILETIME fileTime = value.ToFileTimeUtc();

                SYSTEMTIME systemTime;
                if (!KERNEL32.FileTimeToSystemTime(ref fileTime, out systemTime) || !KERNEL32.SetSystemTime(ref systemTime))
                    throw new Win32Exception();

                return;
            }
        }

        public static string WindowsDirectory
        {
            get { return KERNEL32.GetWindowsDirectory(); }
        }

        /// <summary>
        /// Parses the command line without the intrusive escape semantics of <seealso cref="System.Environment.GetCommandLineArgs"/>.
        /// Individual arguments containing spaces must be enclosed in quotes.
        /// </summary>
        /// <param name="strCommandLine"></param>
        /// <returns></returns>
        public static IEnumerable<string> EnumCommandLineArguments(string strCommandLine = null)
        {
            if (strCommandLine == null)
                strCommandLine = CommandLine;

            int iNextIndex = 0;
            while (true)
            {
                while (iNextIndex < strCommandLine.Length && char.IsWhiteSpace(strCommandLine[iNextIndex]))
                    iNextIndex++;
                if (iNextIndex >= strCommandLine.Length)
                    yield break;

                /// Quoted Argument.
                if (strCommandLine[iNextIndex] == '\"')
                {
                    int iEndIndex = strCommandLine.IndexOf('\"', iNextIndex + 1);
                    if (-1 != iEndIndex)
                    {
                        yield return strCommandLine.SubstringToIndex(iNextIndex + 1, iEndIndex - 1);
                        iNextIndex = iEndIndex + 1;
                    }
                    else
                    {
                        /// Quotes not terminated. Return what remains.
                        yield return strCommandLine.SubstringToIndex(iNextIndex + 1);
                        yield break;
                    }
                }
                /// Unquoted Argument.
                else
                {
                    int iEndIndex = strCommandLine.IndexOf(' ', iNextIndex + 1);
                    if (-1 != iEndIndex)
                    {
                        yield return strCommandLine.SubstringToIndex(iNextIndex, iEndIndex - 1);
                        iNextIndex = iEndIndex + 1;
                    }
                    else
                    {
                        // End of command line. Return what remains.
                        yield return strCommandLine.SubstringToIndex(iNextIndex);
                        yield break;
                    }
                }
            }
        }
    }
}
